package controllers

import (
	"com.opennews.openplatform/web_api/security"
	"com.opennews.openplatform/web_api/services"
	"com.opennews.openplatform/web_api/shared"
	"github.com/gin-gonic/gin"
	"net/http"
	"strings"
)

// Checks on access token to see if it is a valid one and does not expire yet.
// Also checks on url access permission.
func CheckAccessPermission(ctx *gin.Context) {
	urlFullPath := strings.TrimSpace(ctx.FullPath())

	if urlFullPath != "" {
		granted := false

		// Gets authorization from request header.
		// The string should be like below example:
		// Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDU3ODIxNzUsImlhdCI6MTYwNTc3ODU3NSwiaWQiOiJ1c2VyLWlkIiwidXNlcm5hbWUiOiJIdWkiLCJyb2xlcyI6WyJBZG1pbiJdfQ.tKdhYNyeqOS_ITIV_F0sKT6N-NBFjxMkPyXAkzT43WQ
		authorizationHeader := strings.TrimSpace(ctx.GetHeader("Authorization"))

		// Validates token string to see if it is a valid access token and it does not expire yet.
		claims, valid := checkAuthorizationHeader(authorizationHeader)

		// Whatever token is valid or not we still need to check on url access permission.
		granted = checkUrlOnRequestMap(ctx.Request.Method, urlFullPath, claims)

		// If user was authenticated but token is no longer valid or user is not granted for current url 401 code will be sent.
		if (claims != nil && !valid) || !granted {
			AbortWithStatusData(ctx, http.StatusUnauthorized, shared.MapData{"ErrorCode": shared.ErrorUserAccountNotAuthorized})
		}
	}
}

// Checks on authorization header and tris to extract the claims.
func checkAuthorizationHeader(authorizationHeader string) (claims *security.MyClaims, valid bool) {
	authenticationService := services.NewAuthenticationService()

	if authorizationHeader != "" {
		// Fields splits the string authorizationHeader around each instance of one or more consecutive white space characters.
		fields := strings.Fields(authorizationHeader)

		// Checks the string if it contains bearer and token.
		if len(fields) == 2 {
			// Gets token string.
			jsonString := fields[1]

			// Validates token string to see if it is a valid access token and it does not expire yet.
			return authenticationService.ValidateToken(jsonString)
		}
	}

	return nil, false
}

// Checks requested url on security config request map.
func checkUrlOnRequestMap(requestMethod string, url string, claims *security.MyClaims) bool {
	var urlWithoutRoot string
	var configuredUrl string
	granted := false

	// Gets configured web root route name.
	routeRootGroupName := shared.ConfigService.GetAppConfig().Server.RouteRootGroupName

	// Removes root url.
	// For example, url is /web-api/user/get.
	// Url without root is /user/get.
	urlWithoutRoot = strings.Replace(url, routeRootGroupName, "", 1)

	var tempUrl string

	for _, item := range services.CachedRequestMap {
		// If http request method does not match then just ignore.
		if strings.ToUpper(requestMethod) != strings.ToUpper(item.HttpMethod) {
			continue
		}

		// First of all we need to remove trailing wildcard.
		// For example, item.Url is /user/** or /user/*.
		// configuredUrl is /user.
		configuredUrl = strings.Replace(item.Url, "/**", "", 1)
		configuredUrl = strings.Replace(configuredUrl, "/*", "", 1)

		// Removes the configuredUrl from urlWithoutRoot.
		// Before removing, /user/get.
		// After removing, /get.
		tempUrl = strings.Replace(urlWithoutRoot, configuredUrl, "", 1)

		if tempUrl == "" {
			// If tempUrl is empty that means the url is exactly matched with configuredUrl.
			// Then we need to check on item.ConfigAttribute.
			granted = checkUrlOnConfigAttribute(claims, item.ConfigAttribute)

			// If we find exactly match we just need to stop.
			break
		}

		if tempUrl == urlWithoutRoot {
			// If configuredUrl is /user/status then tempUrl will be the same as urlWithoutRoot.
			// That means a mismatch. We need to check on next.
			continue
		}

		if strings.HasSuffix(item.Url, shared.RequestMapUrlDoubleWildcard) {
			// If tempUrl is /get or /status/check it would also match the double wildcard.
			// Then we need to check on item.ConfigAttribute.
			granted = checkUrlOnConfigAttribute(claims, item.ConfigAttribute)
		} else if strings.HasSuffix(item.Url, shared.RequestMapUrlSingleWildcard) {
			// If tempUrl is /get it can match the single wildcard. But /status/check does NOT.
			// Then we need to check on item.ConfigAttribute.
			if strings.Count(tempUrl, "/") == 1 {
				granted = checkUrlOnConfigAttribute(claims, item.ConfigAttribute)
			}
		}
	}

	return granted
}

// Checks requested url access permission on security configAttribute.
// This would check on authentication and authorization.
func checkUrlOnConfigAttribute(claims *security.MyClaims, configAttribute string) bool {
	granted := false

	if strings.HasPrefix(configAttribute, shared.RequestMapPermitAll) {
		granted = true
	} else if strings.HasPrefix(configAttribute, shared.RequestMapIsFullyAuthenticated) && claims != nil {
		granted = true
	} else if (strings.HasPrefix(configAttribute, shared.RequestMapHasRole) ||
		strings.HasPrefix(configAttribute, shared.RequestMapHasAnyRole)) && claims != nil {
		granted = services.NewRequestMapService().ContainsAnyRoleInConfigAttribute(claims.Roles, configAttribute)
	}

	return granted
}
